FBL.ns(function() { with (FBL) {
  
  // -----------------------------------------------------------------
  // this class holds the concept of an image; it holds the
  // following information:
  //  -> number:    a number that is displayed to the user when
  //                shown in the Images Panel
  //  -> file:      a reference to the nsIFile, if it's currently
  //                being edited
  //  -> filename:  name of the file as displayed in the Images Panel
  //                (without the extension)
  //  -> extension: the image type (e.g. 'png')
  //  -> nodes:     an array of objects which hold a node property,
  //                and sometimes a cssRule property (depending on
  //                whether the image being edited comes from an
  //                img tag or a css rule
  //  -> url:       the original url or data url
  //  -> element:   a reference to the image element in the Images
  //                Panel
  // -----------------------------------------------------------------
  function FirepixImage(cfg) {
    // apply all the config properties
    for(var i in cfg) {
      this[i] = cfg[i];
    }
    
    // determine the filename by the url
    this.filename = isDataURL(this.url) ? "anonymous"
                                        : getFilenameFromURL(this.url);
  }
  
  FirepixImage.imageTypes = [
    { mimeType: "image/gif", extension: "gif" },
    { mimeType: "image/jpeg", extension: "jpg" },
    { mimeType: "image/png", extension: "png" },
    { mimeType: "image/bmp", extension: "bmp" },
    { mimeType: "image/tiff", extension: "tiff" }
  ];
  
  FirepixImage.prototype.addInstance = function(imageData) {
    // ensure that the node isn't already in the array of nodes
    for(var i = 0; i < this.nodes.length; ++i) {
      if(this.nodes[i].node == imageData.node) {
        return;
      }
      
      if(imageData.cssRule && this.nodes[i].cssRule == imageData.cssRule) {
        return;
      }
    }
    
    // node isn't already managed, so add it
    this.nodes.push({
      node: imageData.node,
      cssRule: imageData.cssRule
    });
    
    this.updateFilenameDisplay();
  }
  
  FirepixImage.prototype.updateFilenameDisplay = function() {
    var nodeStr = "";
    if(this.nodes.length > 1) {
      nodeStr = " (" + this.nodes.length + ")";
    }
    this.getTextElement().textContent = this.filename + "." + this.extension + nodeStr;
  }
  
  FirepixImage.prototype.remove = function() {
    if(this.file && this.file.exists()) {
      // try to remove the file
      this.file.remove(false);
      
      // refresh image with old url
      this.setUrl();
    }
  }
  
  FirepixImage.prototype.setNumber = function(num) {
    this.number = num;
    this.getNumberElement().textContent = num;
  }
  
  FirepixImage.prototype.setMimeType = function(mimeType) {
    this.mimeType = mimeType;
    
    // find and set the extension
    var types = FirepixImage.imageTypes;
    for(var i = 0; i < types.length; ++i) {
      if(types[i].mimeType == mimeType) {
        this.extension = types[i].extension;
        break;
      }
    }
  }
  
  FirepixImage.prototype.refreshUrl = function() {
    if(this.file && this.file.exists()) {    
      var inputStream = Cc["@mozilla.org/network/file-input-stream;1"]
                          .createInstance(Ci.nsIFileInputStream);
      var binaryInputStream = Cc["@mozilla.org/binaryinputstream;1"]
                                .createInstance(Ci.nsIBinaryInputStream);
      inputStream.init(this.file, 0x01, 0444, null);
      binaryInputStream.setInputStream(inputStream);
      var bytes = binaryInputStream.readBytes(binaryInputStream.available());
      binaryInputStream.close();
      
      var data = "data:" + this.mimeType + ";base64," + window.btoa(bytes);
      this.setUrl(data);
    }
  }
  
  // updates all the nodes in the webpage with the
  // provided data url (or resets it with the original
  // url if none is provided)
  FirepixImage.prototype.setUrl = function(url) {
    if(!url) {
      url = this.url;
    }
    
    for(var i in this.nodes) {
      var nodeObj = this.nodes[i];
      if(nodeObj.cssRule) {
        var cssText = getChildByClass(nodeObj.node, "cssPropValue").textContent;
        cssText = cssText.replace(/url\(.*\)/, "url(\"" + url + "\")");
        
        // copied from Firebug's CSS panel
        var propName = getChildByClass(nodeObj.node, "cssPropName").textContent;
        Firebug.CSSModule.setProperty(nodeObj.cssRule, propName, cssText, "");
      }
      else {
        nodeObj.node.src = url;
      }
    }
  }
  
  FirepixImage.prototype.getNumberElement = function() {
    return this.element.firstChild;
  }
  
  FirepixImage.prototype.getTextElement = function() {
    return this.element.firstChild.nextSibling.firstChild.nextSibling;
  }
  
  // ****************************************************
  //
  //    Helper Functions
  //
  // ****************************************************
  var fpStringBundle = document.getElementById("firepix-string-bundle");
  function L10N(str) {
    return fpStringBundle.getString(str);
  }
  
  function getLocalStorage() {
    var directoryService = Cc["@mozilla.org/file/directory_service;1"]
                            .getService(Ci.nsIProperties);  
    // this is a reference to the profile dir (ProfD) now.  
    var file = directoryService.get("ProfD", Ci.nsIFile);  
    file.append("firepix");  
    
    if (!file.exists() || !file.isDirectory()) {  
      // read and write permissions to owner and group, read-only for others.  
      file.create(Ci.nsIFile.DIRECTORY_TYPE, 0774);  
    }  
    
    // create editors file
    file.append("editors.txt");
    
    if(!file.exists()) {
      file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
    }
    
    return file;
  }
  
  function loadEditors() {
    var file = getLocalStorage();
    
    // open an input stream from file  
    var inputStream = Cc["@mozilla.org/network/file-input-stream;1"]
                        .createInstance(Ci.nsIFileInputStream);
    inputStream.init(file, 0x01, 0666, null);  
    inputStream.QueryInterface(Ci.nsILineInputStream);  
    
    // read lines into array
    var line = {}, hasMoreLines;
    do {
      hasMoreLines = inputStream.readLine(line);
      var editor = line.value.split("\t");
      if(editor[0].length > 0 && editor[1].length > 0) {
        editors.push({
          name: editor[0],
          path: editor[1]
        });
      }
    } while(hasMoreLines);
    
    inputStream.close();
  }
  
  function saveEditors() {
    var file = getLocalStorage();
    
    // file is nsIFile, data is a string
    var outputStream = Cc["@mozilla.org/network/file-output-stream;1"]
                        .createInstance(Ci.nsIFileOutputStream);
    
    // use 0x02 | 0x10 to open file for appending.  
    outputStream.init(file, 0x02 | 0x20, 0666, null);
    
    // write the editors to the file
    var converter = Cc["@mozilla.org/intl/converter-output-stream;1"]
                      .createInstance(Ci.nsIConverterOutputStream);
    converter.init(outputStream, "UTF-8", 0, 0);
    for(var i in editors) {
      converter.writeString(editors[i].name + "\t" + editors[i].path + "\n");
    }
    
    // this closes outputStream
    converter.close();
  }
  
  function makeAjaxHeadRequest(url, callback, scope) {
    var httpRequest;

    if (window.XMLHttpRequest) { // Mozilla, Safari, ...
      httpRequest = new XMLHttpRequest();
      if (httpRequest.overrideMimeType) {
        httpRequest.overrideMimeType("text/xml");
      }
    }

    if (!httpRequest) {
      return false;
    }
    
    httpRequest.onreadystatechange = function() {
      if(httpRequest.readyState == 4) {
        callback.call(scope, httpRequest);
      }
    };
    httpRequest.open("HEAD", url, true);
    httpRequest.send(null);
  }
  
  function getMimeTypeFromDataURL(url) {
    var f = url.match(/(data:)([^;,]+)(.+)/i);
    return f[2];
  }
  
  function getFilenameFromURL(url) {
    // f[2] holds the filename
    var f = url.match(/(\/|\\)([^\/\?\#\.]+)([^\/]*)$/i);
    if(!f || f[2].length == 0) {
      return "unknown";
    }
    
    return f[2];
  }
  
  // copied from firebug
  const reURL = /url\("?([^"\)]+)?"?\)/;
  function parseURLValue(value)
  {
      var m = reURL.exec(value);
      return m ? m[1] : "";
  }
  
  // ****************************************************
  //
  //    Firepix Module
  //
  // ****************************************************
  var panelName = "FirepixImagePanel";
  var editors = [];
  var FirepixModule = extend(Firebug.Module, {
    initialize: function() {
      Firebug.Module.initialize.apply(this, arguments);
      loadEditors();
      this.addContextMenuItems();
    },
    
    initContext: function(context, persistedState) {
      context.firepixImages = [];
    },
    
    destroyContext: function(context, persistedState) {
      var images = context.firepixImages;
      for(var i = 0, imagesLen = images.length; i < imagesLen; ++i) {
        var image = images[i];
        if(image.file && image.file.exists()) {
          // try to remove the image, because the user doesn't need it anymore
          image.file.remove(false);
        }
      }
    },
    
    addContextMenuItems: function() {
      var me = this;
      
      // add context menu for img element
      var elementRep = FirebugReps.Element,
          originalElemFunc = elementRep.getContextMenuItems;
      
      elementRep.getContextMenuItems = function(elt, target, context) {
        var items = [];
        var htmlPanel = this;
        
        if(elt.nodeName.toLowerCase() == "img") {
          items.push({
            label: L10N("contextmenu.manageimage"),
            nol10n: true,
            command: function() {
              var panel = FirebugContext.getPanel(panelName);
              Firebug.chrome.selectPanel(panelName);
              panel.addImage({
                url: elt.src,
                node: elt
              });
            }
          });
        }
        
        return items.concat(originalElemFunc.apply(this, arguments));
      };
      
      // add context menu for css panels (background property)
      var cssPanels = ['css', 'stylesheet'];
      for(var i in cssPanels) {
        var cssPanelPrototype = Firebug.getPanelType(cssPanels[i]).prototype,
            originalCssFunc = cssPanelPrototype.getContextMenuItems;
        
        cssPanelPrototype.getContextMenuItems = function(style, target) {
          var items = [];
          var cssPanel = this;
          
          var cssText = getAncestorByClass(target, "cssPropValue").textContent;
          var containsUrl = (cssText.indexOf("url(") >= 0);
          if(this.infoTipType == "image" || containsUrl) {
            items.push({
              label: L10N("contextmenu.manageimage"),
              nol10n: true,
              command: function() {
                var panel = FirebugContext.getPanel(panelName);
                
                // fix for bug in firebug's getStylesheetURL() function
                var cssRule = Firebug.getRepObject(target);
                if(!cssRule.parentStyleSheet) {
                  cssRule.parentStyleSheet = {};
                }
                var baseURL = cssPanel.getStylesheetURL(cssRule);
                var relURL = parseURLValue(cssText);
                var absURL = isDataURL(relURL) ? relURL : absoluteURL(relURL, baseURL);
                
                Firebug.chrome.selectPanel(panelName);
                panel.addImage({
                  url: absURL,
                  node: getAncestorByClass(target, "cssProp"),
                  cssRule: cssRule
                });
              }
            });
          }
          
          return items.concat(originalCssFunc.apply(this, arguments));
        };
      }
    },
    
    // taken from the "My Image Here" extension
    selectImageFile: function() {
      var nsIFilePicker = Ci.nsIFilePicker;
      var filePicker = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
      
      filePicker.init(window, L10N("dialog.selectimage"), nsIFilePicker.modeOpen);
      filePicker.appendFilters(nsIFilePicker.filterImages);
      
      var result = filePicker.show();
      if (result == nsIFilePicker.returnOK) {
        return filePicker.file;
      }
      
      return null;
    },
    
    // *********************************************
    //
    //  Module's Panel-related Functions
    //
    // *********************************************
    showPanel: function(browser, panel) {
      var isImgPanel = (panel && panel.name == panelName);
      var fpImageButtons = browser.chrome.$("fpImageButtons");
      collapse(fpImageButtons, !isImgPanel);
    },
    
    addStyleSheet: function(doc) {
      if($("fpStyles", doc)) {
        return;
      }
      
      var styleSheet = createStyleSheet(doc, "chrome://firepix/skin/firepix.css");
      styleSheet.setAttribute("id", "fpStyles");
      addStyleSheet(doc, styleSheet);
    },
    
    reattachContext: function(browser, context) {
      var panel = context.getPanel(panelName);
      this.addStyleSheet(panel.document);
    }
  });
  
  // ****************************************************
  //
  //    Image Panel Domplate
  //
  // ****************************************************
  var imageRep = domplate({
    mainTag:
      DIV({ class: "firepix-panel" },
        DIV({ class: "firepix-editors-bar" },
          DIV(
            SPAN("Editors&nbsp;&nbsp;("),
            SPAN({ class: "firepix-editors-link",
                   onclick: "$onClickManage" },
              "manage"
            ),
            SPAN(")")
          ),
          UL({ class: "firepix-editors-list firepix-disabled-list" })
        ),
        DIV({ class: "firepix-images" })
      ),
      
      onClickManage: function() {
        var params = {
          editors: editors,
          editorsChanged: false
        };
        
        window.openDialog(
          "chrome://firepix/content/firepixEditorDialog.xul",
          "firepix-editor-dialog",
          "chrome,modal,centerscreen,resizable", params);
        
        if(params.editorsChanged) {
          // update the global list of editors
          editors = params.editors;
          
          // update the current context's panel's UI
          FirebugContext.getPanel(panelName).updateEditorsList();
          
          // save the list of editors to storage
          saveEditors();
        }
      }
  });
  
  // ****************************************************
  //
  //    Firepix Image Panel
  //
  // ****************************************************
  var FirepixImagePanel = function() {};
  FirepixImagePanel.prototype = extend(Firebug.Panel, {
    name: panelName,
    title: "Images",
    
    initialize: function() {
      Firebug.Panel.initialize.apply(this, arguments);
      
      // ensure that the stylesheet gets added so
      // we can use it in the domplate
      FirepixModule.addStyleSheet(this.document);
      
      // setup UI
      imageRep.mainTag.replace({}, this.panelNode, imageRep);
    },
    
    show: function() {
      // the list of editors must be destroyed and recreated
      // for the panel every time it's shown
      this.updateEditorsList();
    },
    
    addImage: function(imageData) {
      // if the image is already in the list, we don't want
      // to create a duplicate
      var image = this.findImageByUrl(imageData.url);
      if(image) {
        image.addInstance(imageData);
        this.selectImage(image.element);
      }
      else {
        // create the new image, add it to the list in memory
        var images = this.context.firepixImages;
        var newImage = new FirepixImage({
          number: images.length + 1,
          nodes: [{
            node: imageData.node,
            cssRule: imageData.cssRule
          }],
          url: imageData.url
        });
        
        var isDataUrl = isDataURL(newImage.url);
        
        // construct and render the dom elements for the image
        var imageElem = this.document.createElement("div"),
            numElem = this.document.createElement("div"),
            contentElem = this.document.createElement("div"),
            refreshElem = this.document.createElement("div"),
            textElem = this.document.createElement("div"),
            removeElem = this.document.createElement("div"),
            clearElem = this.document.createElement("div");
            
        imageElem.className = "firepix-image";
        numElem.className = "firepix-image-number";
        numElem.textContent = newImage.number;
        contentElem.className = "firepix-image-content";
        var panel = this;
        contentElem.onclick = function() {
          panel.selectImage(imageElem);
        };
        refreshElem.className = "firepix-image-refresh";
        refreshElem.onclick = function(event) {
          newImage.refreshUrl();
          event.stopPropagation();
        }
        textElem.className = "firepix-image-text";
        textElem.innerHTML = "&nbsp;";
        removeElem.className = "firepix-image-remove";
        removeElem.onclick = function(event) {
          panel.removeImage(newImage);
          event.stopPropagation();
        }
        clearElem.className = "clear";
        
        contentElem.appendChild(refreshElem);
        contentElem.appendChild(textElem);
        contentElem.appendChild(removeElem);
        contentElem.appendChild(clearElem);
        imageElem.appendChild(numElem);
        imageElem.appendChild(contentElem);
        this.getImagesElem().appendChild(imageElem);
        this.selectImage(imageElem);
        
        // store the image element so we can make use of it later
        newImage.element = imageElem;
        images.push(newImage);
        
        if(isDataUrl) {
          newImage.setMimeType(getMimeTypeFromDataURL(newImage.url));
          newImage.updateFilenameDisplay();
        }
        else {
          // store the image so the post-ajax function can access it
          this.imageBeingAdded = newImage;
          makeAjaxHeadRequest(newImage.url, this.addImageToUI, this);
        }
      }
    },
    
    addImageToUI: function(httpRequest) {
      if(httpRequest.status == 200) {
        var mimeType = httpRequest.getResponseHeader("Content-Type");
        this.imageBeingAdded.setMimeType(mimeType);
        this.imageBeingAdded.updateFilenameDisplay();
      }
    },
    
    selectImage: function(imageElem) {
      var parentElem = getAncestorByClass(imageElem, "firepix-images");
      
      // unset any other selected image first
      var currImageElem = parentElem.firstChild;
      while(currImageElem != null) {
        removeClass(currImageElem, "firepix-image-selected");
        currImageElem = currImageElem.nextSibling;
      }
      
      // select the clicked image, ensure it's visible, and enable the editors list
      this.currentSelection = imageElem;
      setClass(imageElem, "firepix-image-selected");
      this.ensureVisible(imageElem);
      this.setEditorsListEnabled(true);
    },
    
    editSelectedImage: function(editorPath) {
      // get the image data
      var selectedNum = this.getSelectedImageNum();
      if(selectedNum >= 0) {
        var image = this.getImage(selectedNum);
        
        // try to remove any existing file
        if(image.file) {
          image.file.remove(false);
        }
        
        // create the unique temporary file
        image.file = Cc["@mozilla.org/file/directory_service;1"]
                      .getService(Ci.nsIProperties)
                      .get("TmpD", Ci.nsIFile);
        image.file.append(image.filename + "." + image.extension);
        image.file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
        
        // download the image into the temp file
        var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
                       .createInstance(Ci.nsIWebBrowserPersist);
        persist.saveURI(makeURI(image.url), null, null, null, "", image.file);
        
        // create the editor file
        var editorFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
        editorFile.initWithPath(editorPath);
        
        // launch the image editor
        var process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
        process.init(editorFile);
        var args = [image.file.path];
        process.run(false, args, args.length);
      }
    },
    
    removeImage: function(image) {
      var images = this.context.firepixImages,
          newImages = [];
      
      image.remove();
      for(var i = 0, j = 1, imagesLen = images.length; i < imagesLen; ++i) {
        var currImage = images[i];
        if(currImage != image) {
          newImages.push(currImage);
          currImage.setNumber(j++);
        }
      }
      this.context.firepixImages = newImages;
      
      // if this element is currently selected...
      if(image.element == this.currentSelection) {
        delete this.currentSelection;
        this.setEditorsListEnabled(false);
      }
      
      // remove the element from the dom
      this.getImagesElem().removeChild(image.element);
    },
    
    updateEditorsList: function() {
      var editorsListElem = this.panelNode.getElementsByClassName("firepix-editors-list")[0];
      
      // remove all children
      while(editorsListElem.hasChildNodes()) {
        editorsListElem.removeChild(editorsListElem.firstChild);
      }
      
      // add the editors
      if(editors.length > 0) {
        for(var i in editors) {
          var editorName = editors[i].name;
          var editorPath = editors[i].path;
          var listElem = this.document.createElement("li");
          var linkElem = this.document.createElement("span");
          var me = this;
          linkElem.onclick = (function(path) {
            return function() { me.editSelectedImage(path); }
          })(editorPath);
          linkElem.className = "firepix-editors-link";
          linkElem.innerHTML = editorName;
          listElem.appendChild(linkElem);
          editorsListElem.appendChild(listElem);
        }
      }
      else {
        // this is to help the first-time user with adding editors
        var listElem = this.document.createElement("li");
        listElem.className = "firepix-editors-info";
        listElem.innerHTML = "You don't seem to have any image editors. Click the link above to add some.";
        editorsListElem.appendChild(listElem);
      }
    },
    
    findImageByUrl: function(url) {
      var images = this.context.firepixImages;
      for(var i = 0, imagesLen = images.length; i < imagesLen; ++i) {
        if(images[i].url == url) {
          return images[i];
        }
      }
      
      return null;
    },
    
    ensureVisible: function(elem) {
      var minTop = elem.offsetTop - 7;
      var maxTop = (elem.offsetTop + elem.offsetHeight + 7) - this.getImagesElem().offsetHeight;
      if(this.getImagesElem().scrollTop > minTop) {
        this.getImagesElem().scrollTop = minTop;
      }
      else if(this.getImagesElem().scrollTop < maxTop) {
        this.getImagesElem().scrollTop = maxTop;
      }
    },
    
    setEditorsListEnabled: function(enabled) {
      var listElem = this.document.getElementsByTagName("ul")[0];
      var editorClasses = "firepix-editors-list";
      if(enabled === false) {
        editorClasses += " firepix-disabled-list";
      }
      listElem.className = editorClasses;
    },
    
    getSelectedImageNum: function() {
      if(this.currentSelection) {
        return parseInt(getChildByClass(this.currentSelection, "firepix-image-number").textContent);
      }
      
      return -1;
    },
    
    getImage: function(num) {
      var images = this.context.firepixImages;
      for(var i = 0, imagesLen = images.length; i < imagesLen; ++i) {
        if(images[i].number == num) {
          return images[i];
        }
      }
      
      return null;
    },
    
    getImagesElem: function() {
      if(!this.imagesElem) {
        var panelElem = getChildByClass(this.panelNode, "firepix-panel");
        this.imagesElem = getChildByClass(panelElem, "firepix-images");
      }
      
      return this.imagesElem;
    }
  });
  
  Firebug.registerModule(FirepixModule);
  Firebug.registerPanel(FirepixImagePanel);
}});